/*
 * Copyright (c) 2015 Raytheon BBN Technologies Corp
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.poi.xdgf.usermodel.section.geometry;

import java.awt.geom.AffineTransform;
import java.awt.geom.Arc2D;
import java.awt.geom.Path2D;
import java.awt.geom.Point2D;

import org.apache.poi.POIXMLException;
import org.apache.poi.xdgf.usermodel.XDGFCell;
import org.apache.poi.xdgf.usermodel.XDGFShape;

import com.microsoft.schemas.office.visio.x2012.main.CellType;
import com.microsoft.schemas.office.visio.x2012.main.RowType;

public class EllipticalArcTo implements GeometryRow {
	
	EllipticalArcTo _master = null;
	
	// The x-coordinate of the ending vertex on an arc.
	Double x = null;
	
	// The y-coordinate of the ending vertex on an arc.
	Double y = null;
	
	// The x-coordinate of the arc's control point; a point on the arc. The
	// control point is best located about halfway between the beginning and
	// ending vertices of the arc. Otherwise, the arc may grow to an extreme
	// size in order to pass through the control point, with unpredictable
	// results.
	Double a = null;
	
	// The y-coordinate of an arc's control point.
	Double b = null;
	
	// The angle of an arc's major axis relative to the x-axis of its parent shape.
	Double c = null;
	
	// The ratio of an arc's major axis to its minor axis. Despite the usual
	// meaning of these words, the "major" axis does not have to be greater than
	// the "minor" axis, so this ratio does not have to be greater than 1.
	// Setting this cell to a value less than or equal to 0 or greater than 1000
	// can lead to unpredictable results.
	Double d = null;
	
	Boolean deleted = null;
	
	// TODO: support formulas
	
	public EllipticalArcTo(RowType row) {

		if (row.isSetDel()) deleted = row.getDel();
		
		for (CellType cell: row.getCellArray()) {
			String cellName = cell.getN();
			
			if (cellName.equals("X")) {
				x = XDGFCell.parseDoubleValue(cell);
			} else if (cellName.equals("Y")) {
				y = XDGFCell.parseDoubleValue(cell);
			} else if (cellName.equals("A")) {
				a = XDGFCell.parseDoubleValue(cell);
			} else if (cellName.equals("B")) {
				b = XDGFCell.parseDoubleValue(cell);
			} else if (cellName.equals("C")) {
				c = XDGFCell.parseDoubleValue(cell);
			} else if (cellName.equals("D")) {
				d = XDGFCell.parseDoubleValue(cell);
			} else {
				throw new POIXMLException("Invalid cell '" + cellName + "' in EllipticalArcTo row");
			}
		}
	}
	
	public boolean getDel() {
		if (deleted != null)
			return deleted;
		
		if (_master != null)
			return _master.getDel();
			
		return false;
	}
	
	public Double getX() {
		return x == null ? _master.x : x;
	}
	
	public Double getY() {
		return y == null ? _master.y : y;
	}
	
	public Double getA() {
		return a == null ? _master.a : a;
	}
	
	public Double getB() {
		return b == null ? _master.b : b;
	}
	
	public Double getC() {
		return c == null ? _master.c : c;
	}
	
	public Double getD() {
		return d == null ? _master.d : d;
	}
	
	
	@Override
	public void setupMaster(GeometryRow row) {
		_master = (EllipticalArcTo) row;
	}
	
	public static int draw = 0;

	@Override
	public void addToPath(java.awt.geom.Path2D.Double path, XDGFShape parent) {
	
		if (getDel()) return;
		
		// intentionally shadowing variables here
		double x = getX();
		double y = getY();
		double a = getA();
		double b = getB();
		double c = getC();
		double d = getD();
		
		createEllipticalArc(x, y, a, b, c, d, path);
	}
		
	public static void createEllipticalArc(double x, double y, double a, double b,
										   double c, double d,
										   java.awt.geom.Path2D.Double path) {
		
		// Formula for center of ellipse by Junichi Yoda & nashwaan:
		// -> From http://visguy.com/vgforum/index.php?topic=2464.0
		//
		// x1,y1 = start; x2,y2 = end; x3,y3 = control point
		//
		// x0 = ((x1-x2)*(x1+x2)*(y2-y3)-(x2-x3)*(x2+x3)*(y1-y2)+D^2*(y1-y2)*(y2-y3)*(y1-y3))/(2*((x1-x2)*(y2-y3)-(x2-x3)*(y1-y2)))
		// y0 = ((x1-x2)*(x2-x3)*(x1-x3)/D^2+(x2-x3)*(y1-y2)*(y1+y2)-(x1-x2)*(y2-y3)*(y2+y3))/(2*((x2-x3)*(y1-y2)-(x1-x2)*(y2-y3)))
		// radii along axis:   a = sqrt{ (x1-x0)^2 + (y1-y0)^2 * D^2 }
		//
		
		Point2D last = path.getCurrentPoint();
		double x0 = last.getX();
		double y0 = last.getY();
		
		// translate all of the points to the same angle as the ellipse
		AffineTransform at = AffineTransform.getRotateInstance(-c);
		double[] pts = new double[]{x0, y0, x, y, a, b};
		at.transform(pts, 0, pts, 0, 3);
		
		
		x0 = pts[0]; y0 = pts[1];
		x = pts[2]; y = pts[3];
		a = pts[4]; b = pts[5];
		
		// nasty math time
		
		double d2 = d*d;
		double cx = ((x0-x)*(x0+x)*(y-b)-(x-a)*(x+a)*(y0-y)+d2*(y0-y)*(y-b)*(y0-b))/(2.0*((x0-x)*(y-b)-(x-a)*(y0-y)));
		double cy = ((x0-x)*(x-a)*(x0-a)/d2+(x-a)*(y0-y)*(y0+y)-(x0-x)*(y-b)*(y+b))/(2.0*((x-a)*(y0-y)-(x0-x)*(y-b)));
		
		// calculate radii of ellipse
		double rx = Math.sqrt(Math.pow(x0-cx, 2) + Math.pow(y0-cy,2) * d2);
		double ry = rx / d;
		
		// Arc2D requires us to draw an arc from one point to another, so we
		// need to calculate the angle of the start point and end point along the ellipse
		// - Derived from parametric form of ellipse: x = h + a*cos(t); y = k + b*sin(t) 
		
		double ctrlAngle = Math.toDegrees(Math.atan2((b-cy)/ry, (a-cx)/rx));
		double startAngle = Math.toDegrees(Math.atan2((y0-cy)/ry, (x0-cx)/rx));
		double endAngle = Math.toDegrees(Math.atan2((y-cy)/ry, (x-cx)/rx));
		
		double sweep = computeSweep(startAngle, endAngle, ctrlAngle);
		
		// Now we have enough information to go on. Create the arc.
		Arc2D arc = new Arc2D.Double(cx-rx, cy-ry,
									 rx*2, ry*2, -startAngle, sweep, Arc2D.OPEN);
		
		// rotate the arc back to the original coordinate system
		at.setToRotation(c);
		path.append(at.createTransformedShape(arc), false);
	}
	
	protected static double computeSweep(double startAngle, double endAngle, double ctrlAngle) {
		double sweep;
		
		startAngle = (360.0 + startAngle) % 360.0;
		endAngle = (360.0 + endAngle) % 360.0;
		ctrlAngle = (360.0 + ctrlAngle) % 360.0;
		
		// different sweeps depending on where the control point is
		
		if (startAngle < endAngle) {
			if (startAngle < ctrlAngle && ctrlAngle < endAngle) {
				sweep = startAngle - endAngle;
			} else {
				sweep = 360 + (startAngle - endAngle);
			}	
		} else {
			if (endAngle < ctrlAngle && ctrlAngle < startAngle) {
				sweep = startAngle - endAngle;
			} else {
				sweep = - (360 - (startAngle - endAngle));
			}
		}
		
		return sweep;
	}
}
